/*
Copyright 2025 The Flux authors

Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at

    http://www.apache.org/licenses/LICENSE-2.0

Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/

package controller

import (
	"encoding/json"
	"net/http"
	"net/http/httptest"
	"sync"
	"testing"

	"github.com/stretchr/testify/assert"
	"github.com/stretchr/testify/require"
	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"

	flaggerv1 "github.com/fluxcd/flagger/pkg/apis/flagger/v1beta1"
)

func TestWebhooks(t *testing.T) {
	notReady := flaggerv1.CanaryWebhookPayload{
		Metadata: map[string]string{
			"eventMessage": "podinfo-primary.default not ready: waiting for rollout to finish: 0 out of 1 new replicas have been updated",
		},
	}
	metricsInitialized := flaggerv1.CanaryWebhookPayload{
		Metadata: map[string]string{
			"eventMessage": "all the metrics providers are available!",
		},
	}

	tc := []struct {
		name           string
		expectedEvents map[string][]flaggerv1.CanaryWebhookPayload
		test           func(t *testing.T, mock fixture)
	}{
		{
			name: "skip-analysis",
			expectedEvents: map[string][]flaggerv1.CanaryWebhookPayload{
				"event": {
					metricsInitialized,
					notReady,
					metricsInitialized,
					{Phase: flaggerv1.CanaryPhaseInitialized, Metadata: map[string]string{"eventMessage": "Initialization done! podinfo.default"}},
					{Phase: flaggerv1.CanaryPhaseInitialized, Metadata: map[string]string{"eventMessage": "Confirm-rollout check confirm-rollout-webhook passed"}},
					{Phase: flaggerv1.CanaryPhaseProgressing, Metadata: map[string]string{"eventMessage": "New revision detected! Scaling up podinfo.default"}},
					{Phase: flaggerv1.CanaryPhaseProgressing, Metadata: map[string]string{"eventMessage": "Copying podinfo.default template spec to podinfo-primary.default"}},
					{Phase: flaggerv1.CanaryPhaseSucceeded, Metadata: map[string]string{"eventMessage": "Promotion completed! Canary analysis was skipped for podinfo.default"}},
				},
				"confirm-rollout": {
					{Phase: flaggerv1.CanaryPhaseInitialized, Metadata: map[string]string{"eventMessage": ""}},
				},
			},
			test: func(t *testing.T, mocks fixture) {
				mocks.ctrl.advanceCanary("podinfo", "default")
				mocks.makePrimaryReady(t)
				mocks.ctrl.advanceCanary("podinfo", "default")

				// enable skip
				cd, err := mocks.flaggerClient.FlaggerV1beta1().Canaries("default").Get(t.Context(), "podinfo", metav1.GetOptions{})
				require.NoError(t, err)
				cd.Spec.SkipAnalysis = true
				_, err = mocks.flaggerClient.FlaggerV1beta1().Canaries("default").Update(t.Context(), cd, metav1.UpdateOptions{})
				require.NoError(t, err)

				// update
				dep2 := newDeploymentTestDeploymentV2()
				_, err = mocks.kubeClient.AppsV1().Deployments("default").Update(t.Context(), dep2, metav1.UpdateOptions{})
				require.NoError(t, err)

				// detect changes
				mocks.ctrl.advanceCanary("podinfo", "default")
				mocks.makeCanaryReady(t)

				// advance
				mocks.ctrl.advanceCanary("podinfo", "default")
			},
		},
		{
			name: "canary",
			expectedEvents: map[string][]flaggerv1.CanaryWebhookPayload{
				"event": {
					metricsInitialized,
					notReady,
					metricsInitialized,
					{Phase: flaggerv1.CanaryPhaseInitialized, Metadata: map[string]string{"eventMessage": "Initialization done! podinfo.default"}},
					{Phase: flaggerv1.CanaryPhaseInitialized, Metadata: map[string]string{"eventMessage": "Confirm-rollout check confirm-rollout-webhook passed"}},
					{Phase: flaggerv1.CanaryPhaseProgressing, Metadata: map[string]string{"eventMessage": "New revision detected! Scaling up podinfo.default"}},
					{Phase: flaggerv1.CanaryPhaseProgressing, Metadata: map[string]string{"eventMessage": "Starting canary analysis for podinfo.default"}},
					{Phase: flaggerv1.CanaryPhaseProgressing, Metadata: map[string]string{"eventMessage": "Pre-rollout check pre-rollout-webhook passed"}},
					{Phase: flaggerv1.CanaryPhaseProgressing, Metadata: map[string]string{"eventMessage": "Confirm-traffic-increase check confirm-traffic-increase-webhook passed"}},
					{Phase: flaggerv1.CanaryPhaseProgressing, Metadata: map[string]string{"eventMessage": "Advance podinfo.default canary weight 100"}},
					{Phase: flaggerv1.CanaryPhaseProgressing, Metadata: map[string]string{"eventMessage": "Confirm-traffic-increase check confirm-traffic-increase-webhook passed"}},
					{Phase: flaggerv1.CanaryPhaseProgressing, Metadata: map[string]string{"eventMessage": "Confirm-promotion check confirm-promotion-webhook passed"}},
					{Phase: flaggerv1.CanaryPhaseProgressing, Metadata: map[string]string{"eventMessage": "Copying podinfo.default template spec to podinfo-primary.default"}},
					{Phase: flaggerv1.CanaryPhasePromoting, Metadata: map[string]string{"eventMessage": "Advance podinfo.default primary weight 50"}},
					{Phase: flaggerv1.CanaryPhasePromoting, Metadata: map[string]string{"eventMessage": "Advance podinfo.default primary weight 100"}},
					{Phase: flaggerv1.CanaryPhaseSucceeded, Metadata: map[string]string{"eventMessage": "Post-rollout check post-rollout-webhook passed"}},
					{Phase: flaggerv1.CanaryPhaseSucceeded, Metadata: map[string]string{"eventMessage": "Promotion completed! Scaling down podinfo.default"}},
				},
				"confirm-rollout": {
					{Phase: flaggerv1.CanaryPhaseInitialized, Metadata: map[string]string{"eventMessage": ""}},
				},
				"confirm-promotion": {
					{Phase: flaggerv1.CanaryPhaseProgressing, Metadata: map[string]string{"eventMessage": ""}},
				},
				"confirm-traffic-increase": {
					{Phase: flaggerv1.CanaryPhaseProgressing, Metadata: map[string]string{"eventMessage": ""}},
					{Phase: flaggerv1.CanaryPhaseProgressing, Metadata: map[string]string{"eventMessage": ""}},
				},
				"pre-rollout": {
					{Phase: flaggerv1.CanaryPhaseProgressing, Metadata: map[string]string{"eventMessage": ""}},
				},
				"post-rollout": {
					{Phase: flaggerv1.CanaryPhaseSucceeded, Metadata: map[string]string{"eventMessage": ""}},
				},
				"rollout": {
					{Phase: flaggerv1.CanaryPhaseProgressing, Metadata: map[string]string{"eventMessage": ""}},
				},
			},
			test: func(t *testing.T, mocks fixture) {
				mocks.canary.Spec.Analysis.Interval = "1m"
				mocks.canary.Spec.Analysis.StepWeight = 100
				mocks.canary.Spec.Analysis.StepWeightPromotion = 50
				_, err := mocks.flaggerClient.FlaggerV1beta1().Canaries("default").Update(t.Context(), mocks.canary, metav1.UpdateOptions{})
				require.NoError(t, err)

				// initializing
				mocks.ctrl.advanceCanary("podinfo", "default")

				// make primary ready
				mocks.makePrimaryReady(t)

				// initialized
				mocks.ctrl.advanceCanary("podinfo", "default")
				require.NoError(t, assertPhase(mocks.flaggerClient, "podinfo", flaggerv1.CanaryPhaseInitialized))

				// update
				dep2 := newDeploymentTestDeploymentV2()
				_, err = mocks.kubeClient.AppsV1().Deployments("default").Update(t.Context(), dep2, metav1.UpdateOptions{})
				require.NoError(t, err)

				// detect changes
				mocks.ctrl.advanceCanary("podinfo", "default")
				require.NoError(t, assertPhase(mocks.flaggerClient, "podinfo", flaggerv1.CanaryPhaseProgressing))
				mocks.makeCanaryReady(t)

				// progressing
				mocks.ctrl.advanceCanary("podinfo", "default")
				require.NoError(t, assertPhase(mocks.flaggerClient, "podinfo", flaggerv1.CanaryPhaseProgressing))

				// start promotion
				mocks.ctrl.advanceCanary("podinfo", "default")
				require.NoError(t, assertPhase(mocks.flaggerClient, "podinfo", flaggerv1.CanaryPhasePromoting))

				// end promotion
				mocks.ctrl.advanceCanary("podinfo", "default")
				require.NoError(t, assertPhase(mocks.flaggerClient, "podinfo", flaggerv1.CanaryPhasePromoting))

				// finalising
				mocks.ctrl.advanceCanary("podinfo", "default")
				require.NoError(t, assertPhase(mocks.flaggerClient, "podinfo", flaggerv1.CanaryPhaseFinalising))

				// succeeded
				mocks.ctrl.advanceCanary("podinfo", "default")
				require.NoError(t, assertPhase(mocks.flaggerClient, "podinfo", flaggerv1.CanaryPhaseSucceeded))
			},
		},
	}

	for _, tt := range tc {
		t.Run(tt.name, func(t *testing.T) {
			var mu sync.Mutex
			events := map[string][]flaggerv1.CanaryWebhookPayload{}

			server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
				mu.Lock()
				defer mu.Unlock()

				var payload flaggerv1.CanaryWebhookPayload
				err := json.NewDecoder(r.Body).Decode(&payload)
				require.NoError(t, err)

				// only record the event attributes we're comparing
				eventType := r.URL.Path[1:]
				events[eventType] = append(events[eventType], flaggerv1.CanaryWebhookPayload{
					Phase: payload.Phase,
					Metadata: map[string]string{
						"eventMessage": payload.Metadata["eventMessage"],
					},
				})

				w.WriteHeader(http.StatusOK)
			}))

			defer server.Close()

			c := newDeploymentTestCanary()
			c.Spec.Analysis.Webhooks = []flaggerv1.CanaryWebhook{
				{
					Type: flaggerv1.EventHook,
					Name: "event-webhook",
					URL:  server.URL + "/event",
				},
				{
					Type: flaggerv1.PreRolloutHook,
					Name: "pre-rollout-webhook",
					URL:  server.URL + "/pre-rollout",
				},
				{
					Type: flaggerv1.RolloutHook,
					Name: "rollout-webhook",
					URL:  server.URL + "/rollout",
				},
				{
					Type: flaggerv1.ConfirmPromotionHook,
					Name: "confirm-promotion-webhook",
					URL:  server.URL + "/confirm-promotion",
				},
				{
					Type: flaggerv1.ConfirmRolloutHook,
					Name: "confirm-rollout-webhook",
					URL:  server.URL + "/confirm-rollout",
				},
				{
					Type: flaggerv1.PostRolloutHook,
					Name: "post-rollout-webhook",
					URL:  server.URL + "/post-rollout",
				},
				{
					Type: flaggerv1.ConfirmTrafficIncreaseHook,
					Name: "confirm-traffic-increase-webhook",
					URL:  server.URL + "/confirm-traffic-increase",
				},
			}

			mocks := newDeploymentFixture(c)
			tt.test(t, mocks)

			assert.Equal(t, tt.expectedEvents, events)
		})
	}
}
